package suncertify.db;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import suncertify.application.Log;
import suncertify.application.Strings;
import suncertify.application.exceptions.NullEmptyParameterException;

/**
 * Implements the {@link DBAccess} interface and provides access to the database file data. The class represents the
 * application's database file and should be created once in the application. For database file format details see
 * {@link FileAccess}.
 * 
 * <p>
 * The class uses three member variables:
 * <ol>
 * <li><b>One instance of the {@link FileAccess} class.</b> This class provides the read and write access to the
 * database file. This class uses an internal cache. This class is created in the constructor.</li>
 * <li><b>One instance of the {@link LockManager} class.</b> This class handles the generation of lock cookies and
 * tracks which records are locked with which cookie. This class handles the serialization of client calls if more than
 * one client wants to lock the same record at the same time.</li>
 * <li><b>One instance of the {@link ReadWriteLock} class.</b> This class provides thread safety for the public methods
 * of this class and manages the access from multiple client threads, it allows multiple client threads to retrieve data
 * simultaneously, on the other hand it allows only exclusive access for write clients.</li>
 * </ol>
 * 
 * <p>
 * The class is instantiated using the path to the database file, if the path meets the following conditions:
 * <ol>
 * <li>The file exists in the file system.</li>
 * <li>The file can be accessed for read and write operations.</li>
 * <li>the internal format of the database is valid.</li>
 * </ol>
 * 
 * then the constructor succeeds and the class is instantiated, at this point the internal {@link FileAccess} class
 * instance holds the whole database records in its internal cache. Any read operations like {@link #readRecord(long)}
 * and {@link #findByCriteria(String[])} will not access the data from the database file, instead the data is retrieved
 * from the cache. Write operations like {@link #createRecord(String[])}, {@link #updateRecord(long, String[], long)}
 * and {@link #deleteRecord(long, long)} modify the data in the database file immediately, after that the cache is
 * updated accordingly.
 * 
 * <p>
 * To create an instance of this class use this code sample:
 * 
 * <pre>
 * try {
 *     Data dataAccess = new Data(filePath);
 *     // if you are here then all records from the database file are cached in the Data internal cache.
 *     // the data class is ready to be used at this point.
 *     // IMPORTANT! make sure you create only one instance of this class in you application.
 * } catch (DatabaseFormatException e) {
 *     // if you get this exception then most probably the database file is corrupt or belongs invalid data.
 * } catch (DatabaseAccessException e) {
 *     // If you get this exception then the user running the application does not have enough permission to 
 *     // read and write to the file or the file does not exist.
 * }
 * 
 * </pre>
 * 
 * </p>
 * 
 * <p>
 * To read a data record use this code sample:
 * 
 * <pre>
 * try {
 *     String[] record = data.readRecord(10); // 10 is the record ID
 *     // if you are here then record 10 is read from the cache.
 * } catch (RecordNotFountException e) {
 *     // if you are there then most probably record is already deleted.
 * }
 * </pre>
 * 
 * </p>
 * 
 * <p>
 * To update or delete a record use this code sample:
 * 
 * <pre>
 * // Define your record data in a string array.
 * // You can check the validity of each field using the DataValidator class.
 * // try to prepare and validate the record data before you lock the record.
 * String[] recordData = new String[] { &quot;name&quot;, &quot;location&quot;, &quot;specialties&quot;, &quot;10&quot;, &quot;$20.00&quot;, &quot;10200300&quot; };
 * 
 * // declare a variable for the lock cookie.
 * long lockCookie = -1;
 * 
 * // First lock the contractor and obtain the lockCookie.
 * try {
 *     lockCookie = this.data.lockRecord(10);
 * } catch (RecordNotFoundException e) {
 *     //In this case the record is deleted, most probably by another user in this case return and inform the user.
 * } catch (IllegalStateException e) {
 *     // In this case the server administrator stopped the server while your lock method was executing, in this case inform the user that the server is stopped or terminated.
 * } catch (Exception e) {
 *     // If any other unexpected exception occurs then exit 
 * }
 * 
 * // if you are here then you have a valid lock cookie and you can proceed with the update.
 * try {
 *     this.data.updateRecord(10, recordData, lockCookie);
 * } catch (RecordAlreadyBookedUncheckedException e) {
 *     // In this case you tried to override the booking field of the record, in this case inform the user and exit.
 * } catch (RecordNotFoundException e) {
 *     // Same as above.
 * } catch (suncertify.db.SecurityException e) {
 *     // if you use this code sample then this exception will not happen.
 * } catch (DuplicateKeyUncheckedException e) {
 *     // In this case another record has the same name and location pair as in your recordData array, in this case inform the user to choose a unique name and location.
 * } catch (RecordNotLockedUncheckedException e) {
 *     // if you use this code sample then this exception will not happen.
 * } catch (IllegalArgumentException e) {
 *     // Thrown if the data string array does not contain valid values, you can validate the values before you
 *     // call the update method using the DataValidator class.
 * } catch (IllegalStateException e) {
 *     // same as above, in this case you can do nothing just inform the user.
 * } finally {
 * 
 *     try {
 * 	// DON'T FORGET TO UNLOCK THE RECORD ALWAYS.
 * 	this.data.unlock(newContractor.getID(), lockCookie);
 *     } catch (RecordNotFoundUncheckedException e) {
 * 	// In this case you used an invalid record number.
 *     } catch (suncertify.db.SecurityException e) {
 * 	// Should not happen if you use this sample because you locked the record at the beginning.
 *     } catch (RecordNotLockedUncheckedException e) {
 * 	// Should not happen if you use this sample because you locked the record at the beginning.
 *     } catch (IllegalStateException e) {
 * 	// same as above, in this case you can do nothing just inform the user.
 *     }
 * }
 * </pre>
 * 
 * </p>
 */
public class Data implements DBAccess {

    /**
     * Constant that defines the index of the name field in the data record.
     */
    public static final int FIELD_INDEX_NAME = 0;

    /**
     * Constant that defines the index of the location field in the data record.
     */
    public static final int FIELD_INDEX_LOCATION = 1;

    /**
     * Constant that defines the index of the specialties field in the data record.
     */
    public static final int FIELD_INDEX_SPECIALTIES = 2;

    /**
     * Constant that defines the index of the size field in the data record.
     */
    public static final int FIELD_INDEX_SIZE = 3;

    /**
     * Constant that defines the index of the rate field in the data record.
     */
    public static final int FIELD_INDEX_RATE = 4;

    /**
     * Constant that defines the index of the owner field in the data record.
     */
    public static final int FIELD_INDEX_OWNER = 5;

    /**
     * Constant that defines the owner field's maximum value.
     */
    public static final int FIELD_OWNER_MAXVALUE = 99999999;

    /**
     * Constant that defines the number of fields in the database record.
     */
    public static final int FIELD_COUNT = 6;

    /**
     * Allows multiple reads to read from the database but only one writer, see {@link ReadWriteLock}.
     */
    private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

    /**
     * Generates and manages lock cookies.
     */
    private LockManager lockManager = new LockManager();

    /**
     * Provides the access to the database file.
     */
    private FileAccess fileAccess;

    /**
     * Constant that defines the length of the name record field.
     */
    public static final int FIELD_LENGTH_NAME = 32;

    /**
     * Constant that defines the length of the location record field.
     */
    public static final int FIELD_LENGTH_LOCATION = 64;

    /**
     * Constant that defines the length of the specialties record field.
     */
    public static final int FIELD_LENGTH_SPECIALTIES = 64;

    /**
     * Constant that defines the length of the size record field.
     */
    public static final int FIELD_LENGTH_SIZE = 6;

    /**
     * Constant that defines the length of the rate record field.
     */
    public static final int FIELD_LENGTH_RATE = 8;

    /**
     * Constant that defines the length of the owner record field.
     */
    public static final int FIELD_LENGTH_OWNER = 8;

    /**
     * Determines if the threads accessing the public methods of this class can continue or not. True means the threads
     * can access the data, false means all threads should throw the {@link IllegalStateException} and return
     * immediately. This variable is set to false when the user stops or terminates the server in the server GUI.
     */
    private boolean serverIsRunning = true;

    /**
     * Constructs the data class with the specified parameters. The constructor initializes the internal
     * {@link FileAccess} class which handles the file access. After the constructor's success, all database records are
     * cached in the internal {@link FileAccess} class.
     * 
     * @param filePath
     *            Path of the database file.
     * @throws DatabaseFormatException
     *             Thrown if the database file does not represent a valid application database file.
     * @throws DatabaseAccessException
     *             Thrown if the database file does not exist or an IO error occurs.
     */
    public Data(String filePath) throws DatabaseFormatException, DatabaseAccessException {

	if (filePath == null || filePath.isEmpty()) {
	    throw new NullEmptyParameterException("filePath");
	}

	this.fileAccess = new FileAccess(filePath);

	this.serverIsRunning = true;

	Log.logFine("suncertify.Data() constructor succeeded.");
    }

    /**
     * Creates a record in the database file. The name and location fields of the new record must be unique in the
     * database. The function is thread safe, it calls <code>this.readWriteLock.writeLock().lock()</code> in the
     * beginning and calls <code>readWriteLock.writeLock().unlock();</code> at the end.
     * 
     * @param data
     *            The record fields data. Each filed in this string array represents one record field. The length of the
     *            <code>data</code> array must equal the number of fields in the data record. All fields are mandatory
     *            except the 'owner' field is optional, mandatory fields must not be null or empty strings, the optional
     *            field can be null. The mandatory fields are validated in {@link DataValidator}, each mandatory field
     *            has a corresponding validation function (e.g. isNameValid(), isLocationValid()...etc.), please refer
     *            to these functions for details on the validation rules for each field.
     * @return The record number of the new record. If the database file contains deleted records then the
     *         <code>data</code> is stored in the deleted record with the lowest record number, if the database file
     *         does not contain deleted records then the <cod>data</code> is appended to the end of the database file
     *         and the return value equals the new record count minus one.
     * @throws DuplicateKeyException
     *             Thrown if the record's name and location fields already exist in the database, these two fields must
     *             be unique in the database.
     * 
     * @throws CreateRecordUncheckedException
     *             Thrown if an {@link IOException} is raised.
     * @throws IllegalArgumentException
     *             Thrown if:
     *             <ol>
     *             <li><code>data</code> parameter is null.</li>
     *             <li><code>data</code> length does not equal the number of fields in the record.</li>
     *             <li>{@link DataValidator#isNameValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isLocationValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isSpecialtiesValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isRateValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isSizeValid(String)} returns false.</li>
     *             <li><code>owner</code> is not null and {@link DataValidator#isOwnerValid(String)} returns false.</li>
     *             </ol>
     * 
     * @throws IllegalStateException
     *             Thrown when the {@link #serverIsRunning} class variable is false, this happens when the user stopped
     *             or terminated the server from the server GUI. See {@link #close()} for more details.
     * 
     */
    public long createRecord(String[] data) throws DuplicateKeyException {

	/*
	 * Make sure the data is not null or has incorrect number of fields or has invalid field values. Call this
	 * method before the writeLock because this method does not access shared data.
	 */
	this.isDataValid(data);

	// Synchronize the code by getting the write lock. All code between try and finally access share data that can
	// be changed at any time by another thread.
	readWriteLock.writeLock().lock();

	try {
	    if (!this.serverIsRunning) {
		throw new IllegalStateException(Strings.MSG_SERVER_NOT_RUNNING);
	    }

	    // Check if the name and location values are unique in the database.
	    if (!this.fileAccess.isNameLocationUnique(data[Data.FIELD_INDEX_NAME], data[Data.FIELD_INDEX_LOCATION], -1)) {

		String message = Strings.getMessage(Strings.MSG_RECORD_NOT_UNIQUE_ERROR, data[Data.FIELD_INDEX_NAME],
			data[Data.FIELD_INDEX_LOCATION]);
		throw new DuplicateKeyException(message);
	    }

	    try {
		// Add the new value to the database file and the cache.
		return this.fileAccess.createNewRecord(data);

	    } catch (IOException e) {

		Log.logSevere("IOException in Data.createRecord()");
		String message = Strings.getMessage(Strings.MSG_RECORD_WRITE_IO_ERROR, data[Data.FIELD_INDEX_NAME],
			data[Data.FIELD_INDEX_LOCATION]);
		throw new CreateRecordUncheckedException(message, e);
	    }
	} finally {
	    // IMPORTANT make sure the lock is always released.
	    readWriteLock.writeLock().unlock();
	}
    }

    /**
     * Updates the record with number <code>recNo</code> with the field values specified in <code>data</code> using the
     * lock cookie specified in <code>lockCookie</code> in the database file and the cache. The record must be locked
     * first before calling this function, see {@link #lockRecord(long)}. After this function succeeds the locked record
     * must be released by calling {@link #unlock(long, long)}. The function is thread safe, it calls
     * <code>this.readWriteLock.writeLock().lock()</code> in the beginning and calls
     * <code>readWriteLock.writeLock().unlock();</code> in the end.
     * 
     * @param recNo
     *            The number of the record to be updated in the database. The value must be greater or equal zero and
     *            less than the total number of records in the database.
     * @param data
     *            The record fields data. Each filed in this string array represents one record field. The length of the
     *            <code>data</code> array must equal the number of fields in the data record. All fields are mandatory
     *            except the 'owner' field is optional, mandatory fields must not be null or empty strings, the optional
     *            field can be null. The mandatory fields are validated in {@link DataValidator}, each mandatory field
     *            has a corresponding validation function (e.g. isNameValid(), isLocationValid()...etc.), please refer
     *            to these functions for details on the validation rules for each field.
     * @param lockCookie
     *            The lock cookie obtained from the {@link #lockRecord(long)} function.
     * @throws RecordNotFoundException
     *             Thrown if the <code>recNo</code> is:
     *             <ol>
     *             <li>Less than 0 or larger/equal than/to the database total record count.</li>
     *             <li>Points to a record that is already deleted.</li>
     *             <li>If an {@link IOException} is thrown.</li>
     *             </ol>
     * 
     * @throws DuplicateKeyUncheckedException
     *             Thrown if the record's name and location fields already exist in the database, these two fields must
     *             be unique in the database.
     * 
     * @throws IllegalArgumentException
     *             Thrown if:
     *             <ol>
     *             <li><code>data</code> parameter is null.</li>
     *             <li><code>data</code> length does not equal the number of fields in the record.</li>
     *             <li>{@link DataValidator#isNameValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isLocationValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isSpecialtiesValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isRateValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isSizeValid(String)} returns false.</li>
     *             <li><code>owner</code> is not null and {@link DataValidator#isOwnerValid(String)} returns false.</li>
     *             </ol>
     * 
     * @throws RecordNotLockedUncheckedException
     *             Thrown if the record was not locked before using the {@link #lockRecord(long)}
     * 
     * @throws SecurityException
     *             Thrown if the provided lock cookie is not the same as the cookie retrieved from the
     *             {@link #lockRecord(long)} function used to lock the record.
     * 
     * @throws IllegalStateException
     *             Thrown when the {@link #serverIsRunning} class variable is false, this happens when the user stopped
     *             or terminated the server from the server GUI. See {@link #close()} for more details.
     * 
     * @throws RecordAlreadyBookedUncheckedException
     *             Thrown if the owner field of the <code>data</code> input parameter contains a valid customer ID and
     *             the record specified by <code>recNo</code> also contains a customer ID.
     */
    public void updateRecord(long recNo, String[] data, long lockCookie) throws RecordNotFoundException,
	    SecurityException {

	/*
	 * Make sure the data is not null or has incorrect number of fields or has invalid field values. Call this
	 * method before the writeLock because this method does not access shared data. Try to keep the code between
	 * lock and unlock to a minimum.
	 */
	this.isDataValid(data);

	/*
	 * Acquire writeLock, always release the lock in the finally clause. Only code that access shared data should be
	 * placed between the lock and unlock calls. The record number check access the share record count which could
	 * be changed by other threads. The name and location check also access shared data and needs to be protected.
	 * The cookie check and the write operation also need to be protected because other threads might change them at
	 * any time.
	 */
	readWriteLock.writeLock().lock();

	try {
	    if (!this.serverIsRunning) {
		throw new IllegalStateException(Strings.MSG_SERVER_NOT_RUNNING);
	    }

	    // make sure the record number is valid.
	    this.isRecordNumberValid(recNo);

	    // make sure the record number does not refer to a deleted record.
	    if (this.fileAccess.isRecordDeleted(recNo)) {

		Log.logInfo("Data.updateRecord() attempt to update deleted record " + Long.toString(recNo));

		String message = Strings.getMessage(Strings.MSG_RECORD_ACCESS_DELETED_RECORD, Long.toString(recNo));
		throw new RecordNotFoundException(message);
	    }

	    // check if data wants to update an already booked record.
	    if (!this.canBookRecord(data, recNo)) {
		throw new RecordAlreadyBookedUncheckedException();
	    }

	    // make sure the name and location are still unique
	    if (!this.fileAccess.isNameLocationUnique(data[Data.FIELD_INDEX_NAME], data[Data.FIELD_INDEX_LOCATION],
		    recNo)) {

		String message = Strings.getMessage(Strings.MSG_RECORD_NOT_UNIQUE_ERROR, data[Data.FIELD_INDEX_NAME],
			data[Data.FIELD_INDEX_LOCATION]);
		throw new DuplicateKeyUncheckedException(message);
	    }

	    // make sure the supplied cookie is valid.
	    this.lockManager.isCookieValid(recNo, lockCookie);

	    try {
		this.fileAccess.writeRecord(recNo, data);
	    } catch (IOException e) {
		Log.logInfo("IOException in Data.updateRecord()!");
		String message = Strings.getMessage(Strings.MSG_RECORD_WRITE_IO_ERROR2, Long.toString(recNo));
		throw new RecordNotFoundException(message, e);
	    }
	} finally {
	    readWriteLock.writeLock().unlock();
	}
    }

    /**
     * Determines if the provided record fields can update the record specified by <code>recNo</code>. A contractor
     * record assigned to a customer ID cannot be updated with another customer ID. This method is not synchronized.
     * 
     * @param data
     *            The record fields that are to be saved.
     * @param recNo
     *            The ID of the record to which the data is to be saved.
     * @return <ol>
     *         <li>False if the owner field of the data parameter is not null and not empty string and the record
     *         specified by <code>recNo</code> is also not null or not empty string.</li>
     *         <li>True otherwise</li>
     *         </ol>
     */
    private boolean canBookRecord(String[] data, long recNo) {

	if (data[Data.FIELD_INDEX_OWNER] == null) {
	    return true; // we can always set the owner field to empty
	}

	if (data[Data.FIELD_INDEX_OWNER].trim().isEmpty()) {
	    return true; // we can always set the owner field to empty
	}

	if (this.fileAccess.isRecordBooked(recNo)) {
	    return false; // cannot book an already booked record.
	} else {
	    return true; // can book an empty record.
	}
    }

    /**
     * Deletes the record specified by <code>recNo</code>. The record data is not deleted from the database file, only
     * the record header status is changed to indicate a deleted record. After the record is deleted its physical space
     * is available for new records.
     * 
     * <p>
     * The record to be deleted must be locked first using the {@link #lockRecord(long)} function which provides the
     * <code>lockCookie</code> parameters. After this function succeeds the record must be unlocked to make it available
     * for other clients, this is done by calling the {@link #unlock(long, long)} function.
     * 
     * <p>
     * The function is thread safe, it calls <code>this.readWriteLock.writeLock().lock()</code> in the beginning and
     * calls <code>readWriteLock.writeLock().unlock();</code> in the end.
     * 
     * @param recNo
     *            The number of the record to be deleted in the database. The value must be greater or equal zero and
     *            less than the total number of records in the database.
     * @param lockCookie
     *            The lock cookie obtained from the {@link #lockRecord(long)} function.
     * 
     * @throws RecordNotFoundException
     *             Thrown if the <code>recNo</code> is:
     *             <ol>
     *             <li>Less than 0 or larger/equal than/to the database total record count.</li> <li>Points to a record
     *             that is already deleted.</li><li>If an {@link IOException} is thrown.</li>
     *             </ol>
     * 
     * @throws RecordNotLockedUncheckedException
     *             Thrown if the record was not locked using the {@link #lockRecord(long)} function.
     * 
     * @throws SecurityException
     *             Thrown if the provided lock cookie is not the same as the cookie returned by the
     *             {@link #lockRecord(long)} function used to lock the record.
     * 
     * @throws IllegalStateException
     *             Thrown when the {@link #serverIsRunning} class variable is false, this happens when the user stopped
     *             or terminated the server from the server GUI. See {@link #close()} for more details.
     * 
     */
    public void deleteRecord(long recNo, long lockCookie) throws RecordNotFoundException, SecurityException {

	// Try to get the write lock. All code between the try and finally access shared data.
	readWriteLock.writeLock().lock();

	try {
	    if (!this.serverIsRunning) {
		throw new IllegalStateException(Strings.MSG_SERVER_NOT_RUNNING);
	    }

	    /*
	     * make sure the record number is valid. This code needs to be synchronized because it accesses the record
	     * count shared data.
	     */
	    this.isRecordNumberValid(recNo);

	    // check if an attempt was made to delete a deleted record.
	    if (this.fileAccess.isRecordDeleted(recNo)) {

		Log.logFine("Data.deleteRecord(). Attempt to delete deleted record: " + Long.toString(recNo));
		String message = Strings.getMessage(Strings.MSG_RECORD_ACCESS_DELETED_RECORD, Long.toString(recNo));
		throw new RecordNotFoundException(message);
	    }

	    // Validate the cookie and record number pair. NOTE this function must be called from a synchronized caller.
	    this.lockManager.isCookieValid(recNo, lockCookie);

	    try {
		this.fileAccess.deleteRecord(recNo); // delete the record from the file and from the cache.
	    } catch (IOException e) {
		Log.logSevere("IOException in Data.deleteRecord()");
		String message = Strings.getMessage(Strings.MSG_RECORD_WRITE_IO_ERROR2, Long.toString(recNo));
		throw new RecordNotFoundException(message);
	    }
	} finally {
	    // IMPORTANT! release the write lock.
	    readWriteLock.writeLock().unlock();
	}
    }

    /**
     * Searches the database file for records that match the <code>criteria</code> parameter search criteria. The search
     * criteria is defined in {@link DBAccess#findByCriteria(String[])}. Records that are marked as deleted are excluded
     * from the search. This function searches the values in the internal cache, the database file is not accessed by
     * this function.
     * 
     * <p>
     * The function is thread safe, it calls <code>readWriteLock.readLock().lock();</code> in the beginning and always
     * calls <code>readWriteLock.readLock().unlock();</code> before it returns.
     * 
     * @param criteria
     *            The search criteria as defined in {@link DBAccess#findByCriteria(String[])}.
     * @return Array of record numbers that match the search criteria. If no record matches then a zero length array is
     *         returned.
     * 
     * @throws IllegalArgumentException
     *             Thrown if the <code>criteria</code> parameter is null or the number of members in the array does not
     *             equal the number of fields in the record.
     * 
     * @throws FindByCriteriaRuntimeException
     *             Thrown if any exception is raised during the execution of the function, the causing exception is
     *             available using the {@link Throwable#getCause()}.
     * 
     * @throws IllegalStateException
     *             Thrown when the {@link #serverIsRunning} class variable is false, this happens when the user stopped
     *             or terminated the server from the server GUI. See {@link #close()} for more details.
     * 
     */
    public long[] findByCriteria(String[] criteria) {

	/*
	 * This if statement makes sure the input parameter is not null and contains the correct number of search
	 * fields. This if statement does not need to be synchronized by the readLock because it only operates on the
	 * input parameter and does not read shared data that might be changed by other threads. The shared record field
	 * count parameter read by the fileAccess.getRecordFieldCount() function is only changed once when the database
	 * file is loaded.
	 */
	if ((criteria == null) || (criteria.length != this.fileAccess.getRecordFieldCount())) {

	    String criteriaString = this.convertCriteriaToString(criteria);
	    String fieldCount = Integer.toString(this.fileAccess.getRecordFieldCount());
	    String message = Strings.getMessage(Strings.MSG_RECORD_CRITERIA_ERROR, criteriaString, fieldCount);

	    throw new IllegalArgumentException(message);
	}

	/*
	 * Start of a critical section! all code between lock() and the finally clause accesses shared data.
	 * Synchronization requires system resources, so make sure to make the synchronization lock as short as
	 * possible.
	 */
	readWriteLock.readLock().lock();

	List<Long> matchingRecordsList = new ArrayList<Long>();

	try {
	    if (!this.serverIsRunning) {
		throw new IllegalStateException(Strings.MSG_SERVER_NOT_RUNNING);
	    }

	    // iterate all records that are not deleted.
	    for (long recordID = 0; recordID < this.fileAccess.getRecordCount(); ++recordID) {

		if (this.fileAccess.isRecordDeleted(recordID)) {
		    continue; // skip deleted records.
		}

		// check if the current record matches the search criteria.
		if (this.doesRecordMatchSearchCriteria(this.fileAccess.readRecordFromCache(recordID), criteria)) {

		    matchingRecordsList.add(recordID);
		}
	    }
	} catch (Exception ex) {
	    Log.logFine("Exception caught in Data.findByCriteria()");
	    throw new FindByCriteriaRuntimeException(Strings.MSG_RECORD_FIND_CRITERIA_ERROR, ex);
	} finally {
	    readWriteLock.readLock().unlock();
	}

	/*
	 * End of the critical section! the following code only operates on local function variables that are thread
	 * safe.
	 */

	// convert List<Long> to long[] array
	long[] matchingRecords = new long[matchingRecordsList.size()];

	for (int recordID = 0; recordID < matchingRecordsList.size(); ++recordID) {
	    matchingRecords[recordID] = matchingRecordsList.get(recordID);
	}

	return matchingRecords;
    }

    /**
     * Returns the record fields of the <code>recNo</code> record. The record is returned from the internal cache, no
     * file access is required.
     * 
     * <p>
     * The function is thread safe, it calls <code>readWriteLock.readLock().lock();</code> in the beginning and always
     * calls <code>readWriteLock.readLock().unlock();</code> before it returns.
     * 
     * @param recNo
     *            The number of the record to be deleted in the database. The value must be greater or equal zero and
     *            less than the total number of records in the database.
     * 
     * @return The record as a string array, each element in the array represents one record field value. The order of
     *         the fields in the array is the same as in the database.
     * 
     * @throws RecordNotFoundException
     *             Thrown if the <code>recNo</code> is:
     *             <ol>
     *             <li>Less than 0 or larger/equal than/to the database total record count.</li> <li>Points to a record
     *             that is already deleted.</li>
     *             </ol>
     * 
     * @throws IllegalStateException
     *             Thrown when the {@link #serverIsRunning} class variable is false, this happens when the user stopped
     *             or terminated the server from the server GUI. See {@link #close()} for more details.
     * 
     */
    public String[] readRecord(long recNo) throws RecordNotFoundException {

	readWriteLock.readLock().lock();

	try {
	    if (!this.serverIsRunning) {
		throw new IllegalStateException(Strings.MSG_SERVER_NOT_RUNNING);
	    }

	    this.isRecordNumberValid(recNo);

	    if (this.fileAccess.isRecordDeleted(recNo)) {

		Log.logInfo("Data.readRecord(). Attempt to read deleted recrod " + Long.toString(recNo));

		String message = Strings.getMessage(Strings.MSG_RECORD_ACCESS_DELETED_RECORD, Long.toString(recNo));
		throw new RecordNotFoundException(message);
	    }

	    return this.fileAccess.readRecordFromCache(recNo);

	} finally {
	    readWriteLock.readLock().unlock();
	}
    }

    /**
     * Locks the <code>recNo</code> record. If the record is not locked by another client then a unique lock cookie is
     * returned, the caller can use this cookie to update or delete the record, then the caller must unlock the record
     * so that it becomes available to other callers.
     * 
     * <p>
     * This function is thread safe, if multiple callers from different threads call this function simultaneously with
     * different record numbers then all the calls return and no thread/caller is blocked. If two or more callers from
     * different client threads call this function simultaneously with the same record number then only one
     * thread/caller locks the record and returns, the other threads give up the CPU cycles and get blocked waiting for
     * the record to be unlocked using the {@link #unlock(long, long)} function. The lock/unlock is handled by the
     * {@link LockManager} class. When a record is unlocked then the JVM decides which waiting thread wakes up and gets
     * the record lock.
     * 
     * <p>
     * The lock cookie returned by this function does not mean the locked record is valid, please note when this
     * function returns it might return after being waiting for the record to be released from the another caller that
     * caused it to wait, the first caller might have deleted the record and then unlocked it.
     * 
     * <p>
     * Note the lock function does not access or modify the data in anyway. The locked record numbers and their
     * associated lock cookies are stored and managed by the {@link LockManager} class.
     * 
     * @param recNo
     *            The number of the record to be locked. The value must be greater or equal zero and less than the total
     *            number of records in the database.
     * @return The lock cookie.
     * @throws RecordNotFoundException
     *             Thrown if the <code>recNo</code> is:
     *             <ol>
     *             <li>Less than 0 or larger/equal than/to the database total record count.</li> <li>Points to a record
     *             that is already deleted.</li>
     *             </ol>
     * 
     * @throws IllegalStateException
     *             Thrown when the {@link #serverIsRunning} class variable is false, this happens when the user stopped
     *             or terminated the server from the server GUI. See {@link #close()} for more details.
     */
    public long lockRecord(long recNo) throws RecordNotFoundException {

	readWriteLock.readLock().lock();

	try {
	    if (!this.serverIsRunning) {
		throw new IllegalStateException(Strings.MSG_SERVER_NOT_RUNNING);
	    }

	    this.isRecordNumberValid(recNo);

	    if (this.fileAccess.isRecordDeleted(recNo)) {
		Log.logInfo("Data.lockRecord(). Attempt to lock deleted recrod " + Long.toString(recNo));

		String message = Strings.getMessage(Strings.MSG_RECORD_ACCESS_DELETED_RECORD, Long.toString(recNo));
		throw new RecordNotFoundException(message);
	    }

	} finally {
	    readWriteLock.readLock().unlock();
	}

	/*
	 * Note! make sure the readLock is unlocked before the lockManager.lockRecord is called, the
	 * lockManager.lockRecord() uses an internal Reentrant lock.
	 */

	return this.lockManager.lockRecord(recNo);
    }

    /**
     * Unlocks the specified record using the specified lock cookie. This function is the opposite of the
     * {@link #lockRecord(long)} function. This function is usually called after the {@link #deleteRecord(long, long)}
     * or {@link #updateRecord(long, String[], long)} records function succeeds, this function removes the lock from the
     * record and makes the record available for other clients, even if the update or delete calls fails then this
     * fucntion must be called to unlock the record.
     * 
     * <p>
     * This function causes the JVM to choose one (if any blocked thread(s) exist) of the blocked calling threads to
     * wake up and acquire the lock for <code>recNo</code>.
     * 
     * @param recNo
     *            The number of the record to be locked. The value must be greater or equal zero and less than the total
     *            number of records in the database.
     * 
     * @param cookie
     *            The lock cookie generated by the {@link #lockRecord(long)} function.
     * 
     * @throws RecordNotFoundUncheckedException
     *             Thrown if the <code>recNo</code> is less than 0 or greater/equal than/to record count.
     * 
     * @throws SecurityException
     *             Thrown if the provided lock cookie is not the same as the cookie returned by the
     *             {@link #lockRecord(long)} function used to lock the record.
     * 
     * @throws RecordNotLockedUncheckedException
     *             Thrown if the record was not locked using the {@link #lockRecord(long)} function.
     * 
     * @throws IllegalStateException
     *             Thrown when the {@link #serverIsRunning} class variable is false, this happens when the user stopped
     *             or terminated the server from the server GUI. See {@link #close()} for more details.
     */
    public void unlock(long recNo, long cookie) throws SecurityException {

	readWriteLock.readLock().lock();

	try {
	    if (!this.serverIsRunning) {
		throw new IllegalStateException(Strings.MSG_SERVER_NOT_RUNNING);
	    }

	    this.isRecordNumberValid(recNo);

	    // don't check for deleted record! it is allowed to unlock a deleted
	    // record.
	} catch (RecordNotFoundException e) {
	    Log.logInfo("RecordNotFoundException in Data.unlock() for record " + Long.toString(recNo));
	    throw new RecordNotFoundUncheckedException(e);
	} finally {
	    readWriteLock.readLock().unlock();
	}

	/*
	 * Note! make sure the readLock is unlocked before the lockManager.unlockRecord is called, the
	 * lockManager.unlockRecord() uses an internal Reentrant lock.
	 */
	this.lockManager.unlockRecord(recNo, cookie);
    }

    /**
     * Returns all undeleted records and their corresponding numbers as a key value map. The key of the map is the
     * record number or index in the database file, the value of the map is the records <code>String[]</code>, each
     * element in the string array represents a record field value.
     * 
     * @return A {@link Map}.
     * @throws GetAllRecordsException
     *             Thrown if the execution of the function fails, the causing exception is available using the
     *             {@link Throwable#getCause()}
     * 
     * @throws IllegalStateException
     *             Thrown when the {@link #serverIsRunning} class variable is false, this happens when the user stopped
     *             or terminated the server from the server GUI. See {@link #close()} for more details.
     */
    public Map<Long, String[]> getAllRecords() throws GetAllRecordsException {

	readWriteLock.readLock().lock();

	try {
	    if (!this.serverIsRunning) {
		throw new IllegalStateException(Strings.MSG_SERVER_NOT_RUNNING);
	    }

	    long recordID = 0;
	    HashMap<Long, String[]> records = new HashMap<Long, String[]>();

	    for (recordID = 0; recordID < this.fileAccess.getRecordCount(); ++recordID) {

		if (!this.fileAccess.isRecordDeleted(recordID)) {
		    records.put(recordID, this.fileAccess.readRecordFromCache(recordID));
		}
	    }

	    return records;

	} catch (Exception ex) {

	    Log.logInfo("Exception caught in Data.getAllRecords()!");

	    throw new GetAllRecordsException(Strings.MSG_RECORD_READ_ALL_ERROR, ex);
	} finally {
	    readWriteLock.readLock().unlock();
	}
    }

    /**
     * This function sets the class variable {@link #serverIsRunning} to false which indicates that the server has been
     * stopped or terminated by the user. This function first acquires the class write lock where it sets
     * {@link #serverIsRunning} to false, this causes all threads that are waiting to acquire the read or write lock to
     * throw the {@link IllegalStateException} which indicates that the server is stopped, all public class functions
     * check the member variable immediately after they acquire the class read or write lock.
     * 
     * <p>
     * Secondly the function calls the {@link LockManager#releaseAll()} method which wakes up all threads blocked on
     * records, check this function for more details.
     * 
     * <p>
     * Thirdly the function closes the database file by calling {@link FileAccess#close()}.
     * 
     * <p>
     * At the end the function releases the read write lock.
     * 
     * <p>
     * The main objective of this function is to provide a clean server termination, the server must not terminate while
     * a thread is writing to the database file because this might damage the database file. Also, the server must not
     * terminate when clients are being blocked waiting for a locked record, all waiting threads must be waken up so
     * that their corresponding clients can react safely on the server termination.
     * 
     * @throws DatabaseCloseException
     *             Thrown if any {@link IOException} is thrown which is set as the cause of this exception.
     */
    public void close() throws DatabaseCloseException {

	readWriteLock.writeLock().lock();

	try {
	    this.lockManager.releaseAll();

	    this.serverIsRunning = false;

	    this.fileAccess.close();

	} catch (IOException e) {
	    throw new DatabaseCloseException(Strings.MSG_DATABASE_FILE_CLOSE_ERROR, e);
	} finally {
	    readWriteLock.writeLock().unlock();
	}
    }

    /**
     * Makes sure the <code>recNo</code> is a valid record number.
     * <p>
     * Example: if the number of records in the database is 100 then valid record values are between 0 and 99 only. The
     * values -1, 100, 120 are not valid record numbers.
     * 
     * <p>
     * Note: record numbers of deleted records are also valid record numbers.
     * 
     * @param recNo
     *            The number of the record to be validated.
     * @return True if the <code>recNo</code> is valid. If the number is invalid then the see the throws section.
     * @throws RecordNotFoundException
     *             Thrown if:
     *             <ol>
     *             <li><code>recNo</code> < 0</li> <li><code>recNo</code> >= number of records in the database file.
     *             </li>
     *             </ol>
     */
    private boolean isRecordNumberValid(long recNo) throws RecordNotFoundException {

	if (!this.fileAccess.isRecordNumberValid(recNo)) {

	    String message = Strings.getMessage(Strings.MSG_RECORD_INVALID_ID, Long.toString(recNo));
	    throw new RecordNotFoundException(message);
	}

	return true;
    }

    /**
     * Validates the <code>data</code> parameter which represents a database record. The validation rules are summarized
     * in the throws section below and the {@link DataValidator} functions.
     * 
     * @param data
     *            The record fields data. Each filed in this string array represents one record field. The length of the
     *            <code>data</code> array must equal the number of fields in the data record. All fields are mandatory
     *            except the 'owner' field is optional, mandatory fields must not be null or empty strings, the optional
     *            field can be null. The mandatory fields are validated in {@link DataValidator}, each mandatory field
     *            has a corresponding validation function (e.g. isNameValid(), isLocationValid()...etc.), please refer
     *            to these functions for details on the validation rules for each field.
     * @return True if no {@link IllegalArgumentException} is thrown.
     * 
     * @throws IllegalArgumentException
     *             Thrown if:
     *             <ol>
     *             <li><code>data</code> parameter is null.</li>
     *             <li><code>data</code> length does not equal the number of fields in the record.</li>
     *             <li>{@link DataValidator#isNameValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isLocationValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isSpecialtiesValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isRateValid(String)} returns false.</li>
     *             <li>{@link DataValidator#isSizeValid(String)} returns false.</li>
     *             <li><code>owner</code> is not null and {@link DataValidator#isOwnerValid(String)} returns false.</li>
     *             </ol>
     * 
     */
    private boolean isDataValid(String[] data) {

	if (data == null) {
	    throw new IllegalArgumentException(Strings.MSG_DATA_IllegalArgumentException);
	}

	if (!this.fileAccess.isDataFieldLengthValid(data)) {

	    throw new IllegalArgumentException(Strings.getMessage(Strings.MSG_RECORD_INVALID_FIELD_COUNT,
		    this.fileAccess.getRecordFieldCountAsString()));
	}

	if (!DataValidator.isNameValid(data[Data.FIELD_INDEX_NAME])) {

	    throw new IllegalArgumentException(Strings.MSG_INVALID_FIELD_NAME);
	}

	if (!DataValidator.isLocationValid(data[Data.FIELD_INDEX_LOCATION])) {

	    throw new IllegalArgumentException(Strings.MSG_INVALID_FIELD_LOCATION);
	}

	if (!DataValidator.isSpecialtiesValid(data[Data.FIELD_INDEX_SPECIALTIES])) {

	    throw new IllegalArgumentException(Strings.MSG_INVALID_FIELD_SPECIALTIES);
	}

	if (!DataValidator.isSizeValid(data[Data.FIELD_INDEX_SIZE])) {

	    throw new IllegalArgumentException(Strings.MSG_INVALID_FIELD_SIZE);
	}

	if (!DataValidator.isRateValid(data[Data.FIELD_INDEX_RATE])) {

	    throw new IllegalArgumentException(Strings.MSG_INVALID_FIELD_RATE);
	}

	// check if the optional owner field is provided or not
	if (data[Data.FIELD_INDEX_OWNER] != null) {

	    // if the optional field is provided then make sure it is valid.
	    if (!DataValidator.isOwnerValid(data[Data.FIELD_INDEX_OWNER])) {

		throw new IllegalArgumentException(Strings.MSG_INVALID_FIELD_OWNER);
	    }
	}

	return true;
    }

    /**
     * Determines if the specified record matches the specified search criteria. The search criteria is defined in
     * {@link DBAccess#findByCriteria(String[])}. A record matches the search criteria only if each record field matches
     * its corresponding not null search criteria field.
     * 
     * @param record
     *            The records data string array. Each array element represents a record field.
     * @param searchCriteria
     *            The search criteria elements. Each element in the array corresponds to one field in the record.
     * @return True if the record matches the search criteria, return false otherwise.
     */
    private boolean doesRecordMatchSearchCriteria(String[] record, String[] searchCriteria) {

	// no need to validate the input parameters because this function is
	// private and is only called from one
	// function which takes care of the input parameter validation

	// iterate through all fields.
	for (int i = 0; i < record.length; ++i) {

	    if (searchCriteria[i] != null && record[i].toLowerCase().indexOf(searchCriteria[i].toLowerCase()) != 0) {

		// The first record field that does not match its corresponding
		// criteria field is enough to consider the
		// report not matching.
		return false;
	    }
	}

	return true;
    }

    /**
     * Converts the <code>criteria</code> string array elements into a list of comma separated values.
     * 
     * <p>
     * Example: The criteria:
     * <code>String criteria = new String{"Dogs and Tools", "Some City", null, "", "$10.00"}</code> is converted to:
     * 
     * "Dogs and Tools, Some City, &lt;null&gt;, &lt;emptyString&gt;, $10.00"
     * 
     * @param criteria
     *            The search criteria string array.
     * @return The search criteria as a readable string
     */
    private String convertCriteriaToString(String[] criteria) {

	// Define some string constants.
	final String NULL_PLACEHOLDER = "<null>";
	final String EMPTY_PLACEHOLDER = "<emptyString>";
	final String SEPARATOR = ", ";

	// This string builder is used to build the output string.
	StringBuilder messageCriteria = new StringBuilder("");

	if (criteria == null) {
	    messageCriteria.append(NULL_PLACEHOLDER);
	} else {
	    // iterate each element and append it or append a readable place
	    // holder
	    for (String s : criteria) {

		if (s == null) {
		    messageCriteria.append(NULL_PLACEHOLDER);
		} else if (s.isEmpty())
		    messageCriteria.append(EMPTY_PLACEHOLDER);
		else {
		    messageCriteria.append(s);
		}

		messageCriteria.append(SEPARATOR);
	    }

	    // remove the last comma and space if needed.
	    if (messageCriteria.length() >= SEPARATOR.length()) {

		messageCriteria.replace(messageCriteria.length() - SEPARATOR.length(), messageCriteria.length(), "");
	    }
	}

	return messageCriteria.toString();
    }
}
